package dk.au.cs.peterbn.processing.instanceJobs;

import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import dk.au.cs.peterbn.patterns.DesignPattern;
import dk.au.cs.peterbn.patterns.TypeRelation;
import dk.au.cs.peterbn.patterns.TypeRole;
import dk.au.cs.peterbn.processing.PatternInstance;
import dk.au.cs.peterbn.processing.PerInstanceProcessingJob;
import dk.au.cs.peterbn.processing.ProcessingUtil;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.*;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Created by IntelliJ IDEA.
 * User: peter
 * Date: 11/9/11
 * Time: 2:51 PM
 * To change this template use File | Settings | File Templates.
 */
public class CheckFieldReferences extends PerInstanceProcessingJob{

    private final ProcessingUtil util;

    public CheckFieldReferences(Map<String, PatternInstance> patternInstances, ProcessingUtil util) {
        super(patternInstances);
        this.util = util;
    }

    @Override
    public void process(PatternInstance patternInstance) {
        DesignPattern pattern = patternInstance.getName();
        Set<TypeElement> elements = ElementFilter.typesIn(patternInstance.getElements());
        for (TypeElement typeElement : elements) {
            List<AnnotationMirror> mirrors = util.getPatternAnnotationMirrors(typeElement);
            for (AnnotationMirror mirror : mirrors) {
                TypeRole role = util.getTypeRole(typeElement, mirror);
                Collection<? extends TypeRelation> relations = role.getRelations();
                for (TypeRelation relation : relations) {
                    if (TypeRelation.Kind.HAS_A.equals(relation.getKind())) {
                        checkTypeForField(typeElement, relation.getTarget());
                    }
                }
            }
        }
    }

    private void checkTypeForField(TypeElement typeElement, TypeRole target) {
        List<VariableElement> fields = ElementFilter.fieldsIn(typeElement.getEnclosedElements());
        TypeMirror superclass = typeElement.getSuperclass();
        while (superclass != null && !(superclass instanceof NoType)) {
            TypeElement element = util.asTypeElement(superclass);
            List<VariableElement> elements = ElementFilter.fieldsIn(element.getEnclosedElements());
            for (VariableElement variableElement : elements) {
                if (variableElement.getModifiers().contains(Modifier.PROTECTED) || variableElement.getModifiers().contains(Modifier.PUBLIC)) {
                    fields.add(variableElement);
                }
            }
            superclass = element.getSuperclass();

        }
        boolean isFound = false;
        for (VariableElement field : fields) {
            if (field instanceof Symbol.VarSymbol) {
                Symbol.VarSymbol symbol = (Symbol.VarSymbol) field;

                isFound = checkCompoundType(target, symbol);

            }
        }
        if (!isFound) {
            util.printMessage(Diagnostic.Kind.ERROR, "Missing field reference to role: " + target.getName() , typeElement);
        }
    }

    private boolean checkCompoundType(TypeRole target, Symbol.VarSymbol symbol) {
        Type referencedType = symbol.asType();
        boolean found = false;
        switch (referencedType.getKind()) {
            case ARRAY:
                if (referencedType instanceof ArrayType) {
                    ArrayType type = (ArrayType) referencedType;
                    TypeMirror componentType = type.getComponentType();

                    found = checkSingleType(target, componentType);
                }
                break;
            case DECLARED:
                if (referencedType instanceof DeclaredType) {
                    DeclaredType type = (DeclaredType) referencedType;
                    found = checkSingleType(target, type);
                    if (!found) {
                        List<Symbol.TypeSymbol> typeVars = getTypeVarsFromCollection(symbol, type);
                        for (Symbol.TypeSymbol typeVar : typeVars) {
                            found |= checkSingleType(target, typeVar.asType());
                        }
                    }
                }
                break;
            case TYPEVAR:
                if (referencedType instanceof TypeVariable) {
                    TypeVariable type = (TypeVariable) referencedType;
                    TypeMirror upperBound = type.getUpperBound();
                    found = checkSingleType(target, upperBound);
                }
                throw new IllegalArgumentException("No support for generic fields yet");
            default:
                break;
        }
        return found;
    }

    private List<Symbol.TypeSymbol> getTypeVarsFromCollection(Symbol.VarSymbol symbol, DeclaredType type) {
        TypeElement element = util.asTypeElement(type);
        boolean isCollection = recursiveIsCollection(element, util);
        if (isCollection) {
            return symbol.getTypeParameters();
        }
        return null;
    }

    private boolean recursiveIsCollection(TypeElement element, ProcessingUtil util) {
        List<? extends TypeMirror> interfaces = element.getInterfaces();
        boolean isCollection = false;
        for (TypeMirror anInterface : interfaces) {
            TypeElement typeElement = util.asTypeElement(anInterface);
            if (typeElement.getQualifiedName().toString().equals("java.util.Collection")) {
                isCollection = true;
            } else {
                isCollection |= recursiveIsCollection(typeElement, util);
            }
        }
        return isCollection;
    }

    private boolean checkSingleType(TypeRole target, TypeMirror componentType) {
        boolean found = false;
        TypeElement element = util.asTypeElement(componentType);
        List<AnnotationMirror> mirrors = util.getPatternAnnotationMirrors(element);
        for (AnnotationMirror mirror : mirrors) {
            TypeRole role = util.getTypeRole(element, mirror);
            if (target.getName().equals(role.getName())) {
                found = true;
            }
        }
        return found;
    }
}
